<?php
/**
 * Celebrio Core
 * 
 * @copyright  Copyright (c) 2011 Celebrio Software
 * @package    Celebrio
 * @subpackage Core
 */
namespace Celebrio\Core;

use Nette\Object;
use Nette\String;
use dibi;

use Celebrio\Modules;
use Celebrio\Core\IModuleInit;
use Celebrio\Core\VersionComparator;

use Celebrio\FileSystem\Folder;

/**
 * Dummy ModuleInit implementation, without install or init, always available,
 * not changing the config, without config form fields.
 */
class DummyInit extends Object implements IModuleInit {

    const UP = "up";
    const DOWN = "down";
    const NAME_DELIMITER = "_";
    
    const UNINSTALL = "uninstall";
    const UPGRADE = "upgrade";
    const INSTALL = "install";
    
    /**
     * Installed version of a module
     * 
     * @var string 
     */
    private $installedVersion;
    
    
    /**
     *
     * @var type 
     */
    private $moduleName;
    
    
    /**
     * Performs installation, sql files are taken from from standard SQL directory
     * @param array $configuration 
     */
    public function install(array $configuration) {
        $sqlDirName = $this->getStandardSqlDirectory();
        $this->standardInstall($sqlDirName);
    }

    
    /**
     * Performs installation, sql files are taken from from standard SQL directory
     */
    public function uninstall() {
        $sqlDirName = $this->getStandardSqlDirectory();
        $this->standardUninstall($sqlDirName);
    }
    
    
    /**
     * Upgrades the module to the latest version, sql files are taken from standard SQL directory
     */
    public function upgrade() {
        $sqlDirName = $this->getStandardSqlDirectory();
        $this->standardUpgrade($sqlDirName);
    }
    
    /**
     * Performs installation, sql files are taken from specified directory,
     * sets the installed version to class property
     * 
     * @param string $sqlDirName 
     * @throws \Exception 
     */
    protected function standardInstall($sqlDirName) {     
        if (is_dir($sqlDirName)) {
            $dbName = $this->specifyDatabase();
            $scripts = $this->getSqlScripts($dbName, self::UP, $sqlDirName);
            $sortedScripts = $this->sortScriptsByVersion($scripts, true);
            $this->installedVersion = $this->runScripts($sortedScripts, $sqlDirName, self::INSTALL);
        } else {
            throw new \Exception("Given SQL directory $sqlDirName is not correct.");
        }
    }
    
    
    /**
     * Performs uninstallation, sql files are taken from specified directory,
     * sets the installed version to null
     * 
     * @param type $sqlDirName
     * @throws \Exception 
     */
    protected function standardUninstall($sqlDirName) {
        if (is_dir($sqlDirName)) {
            $dbName = $this->specifyDatabase();
            $scripts = $this->getSqlScripts($dbName, self::DOWN, $sqlDirName);
            $sortedScripts = $this->sortScriptsByVersion($scripts, false);
            $currentVersion = $this->getCurrentlyInstalledVersion($this->moduleName);
            $this->runScripts($sortedScripts, $sqlDirName, self::UNINSTALL, $currentVersion);
            $this->installedVersion = null;
        } else {
            throw new \Exception("Given SQL directory $sqlDirName is not correct.");
        }
    }
    
    /**
     * Performs upgrade, sql files are taken from specified directory,
     * sets the installed version to class property
     * 
     * @param type $sqlDirName
     * @throws \Exception 
     */
    protected function standardUpgrade($sqlDirName) {
        if (is_dir($sqlDirName)) {
            $dbName = $this->specifyDatabase();
            $scripts = $this->getSqlScripts($dbName, self::UP, $sqlDirName);
            $sortedScripts = $this->sortScriptsByVersion($scripts, true);
            $currentVersion = $this->getCurrentlyInstalledVersion($this->moduleName);
            $this->installedVersion = $this->runScripts($sortedScripts, $sqlDirName, self::UPGRADE, $currentVersion);
        } else {
            throw new \Exception("Given SQL directory $sqlDirName is not correct.");
        }
    }
    
    public function init(array $configuration) {
        // no init
    }
    
    public function isAvailable() {
        // always available
        return true;
    }
    
    public function formFields() {
        // no config form
        return array();
    }
    
    public function changeConfig(array $configuration) {
        // no changes in config
        return $configuration;
    }
    
    public function upgradeConfig(array $configuration) {
        // no changes in config
        return $configuration;
    }
    
    
    /** 
     * Finds out which database is Celebrio running on
     * 
     * @return string DBNAME
     * Known DBNAMEs are "Postgre", "MySQL", "MSSQL" and "Azure".
     */
    private function specifyDatabase() {
        $dbName = ServiceProvider::getServiceName(Modules::DB);
        return $dbName;
    }
    
    
    /**
     * Gets all relevant(right direction and database) sql scripts for installation, uninstallation 
     * or another version change(upgrade, downgrade).
     * 
     * @param string $dbName name of the database, should be provided by using specifyDatabase() function
     * @param string $direction "up" for install and upgrade, "down" for uninstal and maybe later downgrade
     * @param string $sqlDirName directory where sql files are located
     * @return array names of sql scripts which will be used in (un)install or up/downgrade in associative array, 
     * where versions are the keys
     */
    private function getSqlScripts($dbName, $direction, $sqlDirName) {   
        // add slash to the end if it's not present
        if (!String::endsWith($sqlDirName, "/")) {
            $sqlDirName .= "/";
        }    
        //get directory contents and filter only relevant files
        $sqlDir = new Folder($sqlDirName);
        $scripts = $sqlDir->getContent();
        $filteredScripts = $this->filterScripts($dbName, $direction, $scripts);
        
        if(empty($filteredScripts)) {
            throw new \Exception("SQL directory doesn't contain any SQL scripts that could be used.");
        }
        return $filteredScripts;     
    }
    
    
    /**
     * Sorts the names of scripts by their versions
     * 
     * @param array $scripts array scripts to sort in format: version => scriptName
     * @param bool $ascending defines whether the sort will be ascending or descending 
     * @return array Sorted scripts 
     */
    private function sortScriptsByVersion($scripts ,$ascending = true) {
        $comparator = new VersionComparator();
        if($ascending) {
            $sortFunction = array($comparator, 'compareVersionsAsc');
        } else {
            $sortFunction = array($comparator, 'compareVersionsDesc');
        }
        
        uksort($scripts, $sortFunction);
        return $scripts;        
    }
    
    
    
    /**
     * Executes given SQL scripts from given directory 
     * @param array $sortedScripts
     * @param string $sqlDirName 
     * @param string $version Version (key) to run scripts from, NULL if all scripts should be executed 
     * 
     * @return string Last installed version
     */
    private function runScripts($sortedScripts, $sqlDirName, $operation, $currentVersion = null) {
        if(empty($sortedScripts)) {
            throw new \Exception("There are no SQL scripts to run.");
        }
        //return value - last installed version
        $installedVersion = null;
        
        //script will be executed only if $execute is true, for install, all scripts should be executed
        $execute = false;     
        if($operation === self::INSTALL) {
            $execute = true;
        }
        
        foreach ($sortedScripts as $version => $script) {            
            //if execute is false, check whether the version of the script is correct and if it is, set execute to true
            //otherwise continue on the next script
            if(!$execute) {
                if(VersionComparator::compareVersionsAsc($version, $currentVersion) == 0) {
                    //the version was correct, check whether it is installing or uninstalling and execute (uninstall) 
                    //or continue and execute (upgrade) scripts
                    $execute = true;
                    if($operation === self::UPGRADE) {
                        continue;
                    }
                } else {
                    //the version was not correct -> continue to the next script
                    continue;
                }
            }            
            dibi::begin();
            DDL::execute($sqlDirName . $script);
            dibi::commit();
            $installedVersion = $version;
        }
        if (!$execute) {
            throw new \Exception("SQL scripts couldn't be executed. Check whether there are correct scripts in your SQL directory");
        }
        return $installedVersion;
    }
    
    
    /**
     * Returns the standard SQL directory  "./sql/"
     * @return string
     * @throws \Exception if SQL direcotry is not correct
     */
    protected function getStandardSqlDirectory() {
        $sqlDirName = dirname($this->getReflection()->getFileName()) . "/sql/";
        if (is_dir($sqlDirName)) {
            return $sqlDirName;
        } else {
            throw new \Exception("Standard SQL directory $sqlDirName is not correct.");
        }
    }
    
    
    /**
     * Gets the latest version from standard directory
     * @param type $moduleName 
     */
    public function getLatestVersion() {
        try {
            $sqlDirName = $this->getStandardSqlDirectory();
            $dbName = $this->specifyDatabase();
            $scripts = $this->getSqlScripts($dbName, self::UP, $sqlDirName);
            $sortedScripts = $this->sortScriptsByVersion($scripts, false);
            if(!empty($sortedScripts)) {
                $keys = array_keys($sortedScripts);
                return $keys[0];
            } else {
                return null;
            }    
        } catch (\Exception $e) {
            return "unversioned";
        }
    }
    
    
    /**
     * Gets currenly installed version of a specified module from installed.ini file
     * 
     * @param string $moduleName Name of the module
     * @return string Version of the specified module
     * @throws \Exception 
     */
    public function getCurrentlyInstalledVersion($moduleName) {
        $installedModules = Config::current()->loadConfigArray("installed");
        if(!isset($installedModules[$moduleName])) {
            throw new \Exception("Module $moduleName has never been installed.");
        }
        $moduleStatus = $installedModules[$moduleName];
        $parsedStatus = explode(ConfigManagement::VER_DELIMITER, $moduleStatus);
        $version = null;
        
        switch (count($parsedStatus)) {
            case 1:
                if($parsedStatus[0] == ConfigManagement::OFF) {
                    throw new \Exception("Module $moduleName is not installed.");
                } else {
                    throw new \Exception("Entry about module $moduleName is in wrong format.");
                }
                break;
            
            case 2:
                if($parsedStatus[0] == ConfigManagement::ON) {
                    $version = $parsedStatus[1];
                } else {
                    throw new \Exception("Entry about module $moduleName is in wrong format.");
                }
                break;

            default:
                throw new \Exception("Entry about module $moduleName is in wrong format.");
                break;
        }
        
        return $version;
    }
    
    
    /**
     * Filters array of script names by given criteria: direction and database name.
     * Returns an associative array, where versions of scripts (specified in script names) are the keys.
     * 
     * If the database name is "Azure" or "MSSQL", returns script names with either "Azure" or "MSSQL"
     * and all scripts with name "MSSQLCloud"
     * 
     * Script names have pattern:
     * [moduleName]_[databaseType]_[versionInfo]_[direction].sql
     * 
     * @param type $dbName
     * @param type $direction
     * @param type $scripts
     * @return type 
     */
    private function filterScripts($dbName, $direction, $scripts) {
        $filteredScripts = array();
        $azureName = "Azure";
        $mssqlName = "MSSQL";
        $cloudName = "MSSQLCloud";
        foreach ($scripts as $scriptName) {
            $parsedName = explode(self::NAME_DELIMITER, $scriptName);
            $s_dbName = $parsedName[1];
            $s_version = $parsedName[2];
            $directionToProcess = explode(".", $parsedName[3]);
            $s_direction = $directionToProcess[0];
            
            //if the ending of file name isnt "sql", pass thes file
            if($directionToProcess[1] !== "sql") {
                continue;
            }
            
            //i know this may look horrible but what it does is this:
            //first check, whether it is correct direction and then check whether it is correct name of database
            //if the name of used database is "Azure" or "MSSQL" then also accept "MSSQLCloud" which is used
            //in both of these databases. If everything is fine, add this script name to filtered scripts
            //and set its key to its version for easier processing 
            if ($s_direction == $direction) {
                if ($s_dbName == $dbName ||
                        ( ($dbName == $azureName || $dbName == $mssqlName) && $s_dbName == $cloudName )
                ) {
                    $filteredScripts[$s_version] = $scriptName;
                }
            }
        }
        return $filteredScripts;
    }
    
    
    protected function setInstalledVersion($version) {
        $this->installedVersion = $version;
    }
    
    public function getInstalledVersion() {
        return $this->installedVersion == null ? "unversioned" : $this->installedVersion;
    }
    
    public function setModuleName($moduleName) {
        $this->moduleName = $moduleName;
    }
    
    public function getModuleName() {
        return $this->moduleName;
    }
    
}